using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;

namespace VerticalProcesses
{
    /// <summary>
    /// This class handles parsing the input GRUinfo file and creating the required GRUs. It also provides
    /// a changelog of dates and specific GRUs that change during the simulation period.
    /// </summary>
    public class GRUBuilder
    {
        
        /// <summary>
        /// F = Forest
        /// C = Clearcut
        /// G = Glacier
        /// L = Lake
        /// W = Wetland
        /// </summary>
        private string[] AvailableLanduseTypes = new string[5] { "F", "C","G", "L", "W" };     
        private ArrayList ColumnNames;                              // arraylist of column names 
        private Dictionary<string, double> gruGlobalParameters;     // Dictionary that holds gru parameters  
        private double timeStepHrs;
        private string evapoEq = "NULL";                            // name of the equation to use for ETpot
        private string snowEngergyInputEq = "NULL";                 // name of the equation to use for snowmelt engergy input
        private string canopyType = "NULL";                         // name of the type of canopy to build
        private string snowpackType = "NULL";                       // name of the type of snowpack to build
        private string soilType = "NULL";                       // name of the type of soil to build

        /// <summary>
        /// A simple structure for storing GRU changes found in the GRUinfo file
        /// </summary>
        public struct GRUChange
        {
            public DateTime ChangeDate;
            public List<string> GRUID;
            public List<string> landuseType;
            public List<Dictionary<string,double>> Parameters;
        };

        
        #region Properties

        public string SimulationName { get; set; } // The name of the watershed, or simulation
        public List<GRUChange> GRUChangeLog { get; set; }// List of changes to GRUs through time associated with model
        public List<GRUClass> GRUList { get; set; }// A list of all GRUs within the watershed simulation. Each element of the list is of GRUClass.
  

        //overloaded indexers

        //allow for array type index

        public GRUClass this[int index]
        {
            get
            {
                return GRUList[index];

            }
        }

        //index on string (ID)
        private int findID(string ID)
        {
            for (int i = 0; i < GRUList.Count; i++)
            {
                if (Convert.ToString(GRUList[i].GRUID) == ID)
                {
                    return i;
                }
            }
            throw new Exception("GRU list does not contain this ID");
        }

        public GRUClass this[string index]
        {
            get
            {
                return this[findID(index)];
            }
        }

        #endregion

        public GRUBuilder(string GRUInfoFile, Dictionary<string, double> gruGlobalParameters, double timeStepHrs, string evapoEq, string snowEngergyInputEq, string canopyType, string snowpackType, string soilType)
        {
            
            this.gruGlobalParameters = gruGlobalParameters;
            this.timeStepHrs = timeStepHrs;
            this.evapoEq = evapoEq;
            this.snowEngergyInputEq = snowEngergyInputEq;
            this.canopyType = canopyType;
            this.snowpackType = snowpackType;
            this.soilType = soilType;

            //read gru info file
            StreamReader srGruInfo = new StreamReader(GRUInfoFile);
            string contentInfo = srGruInfo.ReadToEnd();
            srGruInfo.Close();
            GRUList = new List<GRUClass>();
            GRUChangeLog = new List<GRUChange>();
            ParseGRUFile(contentInfo);
        }

        #region Public methods
        /// <summary>
        /// Get GRU IDs corresponding to order in GRU list
        /// </summary>
        /// <returns></returns>
        public Dictionary<string, bool> GetIDs()
        {
            Dictionary<string, bool> IDs = new Dictionary<string, bool>();

            
            for (int i = 0; i < GRUList.Count; i++)
            {
                if (GRUList[i].Write)
                    IDs.Add(GRUList[i].GRUID, GRUList[i].Write);
            }
            return IDs;
        }

        /// <summary>
        /// Checks if there are any changes to gru(s)
        /// </summary>
        /// <param name="currentTimestep"></param>
        /// <param name="changeLog"></param>
        public void CheckForChanges(DateTime currentTimestep)
        {

            foreach(GRUChange change in GRUChangeLog)
            {
                if (currentTimestep == change.ChangeDate)
                {
                    for (int i = 0; i < change.GRUID.Count; i++ )
                    {
                        foreach (GRUClass gru in GRUList)
                        {
                           if(gru.GRUID==change.GRUID[i])
                            gru.Change(change.Parameters[i]);
                        }

                    }
                   

                }

            }        
        
        }

        #endregion

        #region Private methods


        private void ParseGRUFile(string contents)
        {
            Regex reg = new Regex(":GRUdescription\\s+", RegexOptions.IgnoreCase);
            string[] fileBlocks = reg.Split(contents);
            if (fileBlocks.Length == 1) // split on :GRUdescription probably failed.
            {
                throw new Exception("GRU info file does not contain required information!");
            }
                       
            // now process the Header (the first element)
            parseHeader(fileBlocks[0]);

            // now parse and create GRUs
            createGRUs(fileBlocks[1]);

            // now capture any changes to GRUs in changeLog
            if (fileBlocks.Length > 2)
            {
                for (int i = 2; i < fileBlocks.Length; i++)
                {
                    captureGRUchanges(fileBlocks[i]);
                }
            }
        }

        /// <summary>
        /// This routine handles parsing the header of a GRUinfo file
        /// </summary>
        /// <param name="headerBlock"></param>
        private void parseHeader(string headerBlock)
        {
            // split based on CRLF
            string[] lines = Regex.Split(headerBlock, @"\r\n");
            foreach (string s in @lines)
            {
                string stripped = stripComments(s);

                // split on colon
                string[] keyval = Regex.Split(stripped, ":");
                if (keyval.Length == 2)
                {
                    switch (@keyval[0].Trim().ToLower())
                    {
                        case "name":
                            SimulationName = keyval[1].Trim();
                            break;
                        case "columnnames": 
                            string[] colNames = Regex.Split(keyval[1].Trim().ToLower(), "\\s+");
                            ColumnNames = new ArrayList();
                            foreach (string n in colNames) ColumnNames.Add(n);
                            break;
                    }
                }
            }

          

            // confirm that we have appropriate information from header
            if (ColumnNames.Count < 1)
            {
                throw new Exception("Error parsing header");
            }         
            
        }

        /// <summary>
        /// This routine reads the GRUdescription block and instantiates GRUs of the type
        /// represented in the column GRUclass
        /// </summary>
        /// <param name="gruBlock"></param>
        private void createGRUs(string fileBlock)
        {
            string[] lines = Regex.Split(fileBlock, "\r\n");
            int ctr = 0;
            foreach (string line in lines)
            {
                string l = stripComments(line).Trim();
                string[] gruInfo = Regex.Split(l, "\\s+");
                if (gruInfo.Length == ColumnNames.Count)
                {
                    GRUClass gru = parseGRUClass(gruInfo);
                    GRUList.Add(gru);
                    ctr++;

                }
            }
        }

        /// <summary>
        /// This routine does the heavy-lifting for recording the history of changes to 
        /// a particular GRU through time.
        /// </summary>
        /// <param name="fileBlock"></param>
        private void captureGRUchanges(string fileBlock)
        {

            string[] lines = Regex.Split(fileBlock, "\r\n");
            GRUChange myChange = new GRUChange();
            myChange.GRUID = new List<string>();
            myChange.landuseType = new List<string>();
            myChange.Parameters = new List<Dictionary<string, double>>();
            myChange.ChangeDate = Convert.ToDateTime(lines[0]); // the first line better be a datetime                           
            
 
            // loop through all grus to be changed
            for (int i = 1; i < lines.Length; i++)
            {
                string line = stripComments(lines[i].Trim());
                string[] gruInfo = Regex.Split(line, "\\s+");
                if (gruInfo.Length == ColumnNames.Count)
                {
                    string id = "NULL";
                    string landuseType = "NULL";
                    double area = double.NaN;
                    double latitude = double.NaN;
                    double longitude = double.NaN;
                    double timeZone = double.NaN;
                    double elevation = double.NaN;
                    double slope = double.NaN;
                    double aspect = double.NaN;
                    bool write = false;
                    string climateStation = "NULL";                            
                    Dictionary<string, double> parameters = new Dictionary<string, double>();

                    for (int j = 0; j < ColumnNames.Count; j++)
                    {
                        string name = (string)ColumnNames[j];
                        switch (name)
                        {
                            case "id": id = Convert.ToString(gruInfo[j]); break;
                            case "landusetype": landuseType = Convert.ToString(gruInfo[j]); break;
                            case "longitude": longitude = Convert.ToDouble(gruInfo[j]); break;
                            case "latitude": latitude = Convert.ToDouble(gruInfo[j]); break;
                            case "timezone": timeZone = Convert.ToDouble(gruInfo[j]); break;
                            case "z": elevation = Convert.ToDouble(gruInfo[j]); break;
                            case "area": area = Convert.ToDouble(gruInfo[j]); break;
                            case "slope": slope = Convert.ToDouble(gruInfo[j]); break;
                            case "aspect": aspect = Convert.ToDouble(gruInfo[j]); break;
                            case "output": write = Convert.ToBoolean(gruInfo[j].ToLower()); break;
                            case "climatestation": climateStation = gruInfo[j]; break;
                            default:
                                parameters.Add(name, Convert.ToDouble(gruInfo[j]));
                                break;
                        }
                    }

                    myChange.GRUID.Add(id);
                    myChange.landuseType.Add(landuseType);
                    myChange.Parameters.Add(parameters);
                    
                }
            }
            GRUChangeLog.Add(myChange);
        }



        /// <summary>
        /// This routine does the majority of the heavy-lifting in instantiating the GRUs
        /// </summary>
        /// <param name="gruInfo">An array from GRUinfo file representing GRU information</param>
        /// <returns>An instantiated GRUClass</returns>
       
        private GRUClass parseGRUClass(string[] gruInfo)
        {
            string id = "NULL";
            string landuseType = "NULL";
            double area = double.NaN;
            double latitude = double.NaN;
            double longitude = double.NaN;
            double timeZone = double.NaN;
            double elevation = double.NaN;
            double slope = double.NaN;
            double aspect = double.NaN;
            bool write = false;
            string climateStation = "NULL";
            Dictionary<string, double> gruLocalParameters = new Dictionary<string, double>();

            for (int i = 0; i < ColumnNames.Count; i++)
            {                
                string name = (string)ColumnNames[i];
                switch (name)
                {
                    case "id": id = Convert.ToString(gruInfo[i]); break;
                    case "landusetype": landuseType = Convert.ToString(gruInfo[i]); break;
                    case "longitude": longitude = Convert.ToDouble(gruInfo[i]); break;
                    case "latitude": latitude = Convert.ToDouble(gruInfo[i]); break;
                    case "timezone": timeZone = Convert.ToDouble(gruInfo[i]); break;   
                    case "z": elevation = Convert.ToDouble(gruInfo[i]); break;
                    case "area": area = Convert.ToDouble(gruInfo[i]); break;
                    case "slope": slope = Convert.ToDouble(gruInfo[i]); break;
                    case "aspect": aspect = Convert.ToDouble(gruInfo[i]); break;
                    case "output": write = Convert.ToBoolean(gruInfo[i].ToLower()); break;
                    case "climatestation": climateStation = gruInfo[i]; break;
                    default:
                        gruLocalParameters.Add(name, Convert.ToDouble(gruInfo[i]));
                        break;
                }
            }

     

            // instantiate the class (this code MUST be modified as 
            // new GRU types are derived from GRUClass 
            GRUClass gru;
            switch (landuseType.ToUpper())
            {
                case "OP":
                    gru = new GRUClearcut(timeStepHrs, id, latitude, longitude, elevation, area, slope, aspect, timeZone, write, climateStation,
                        evapoEq, snowEngergyInputEq, canopyType, snowpackType, soilType, gruGlobalParameters, gruLocalParameters); break;
                case "TC":
                    gru = new GRUForest(timeStepHrs, id, latitude, longitude, elevation, area, slope, aspect, timeZone, write, climateStation,
                                     evapoEq, snowEngergyInputEq, canopyType, snowpackType, soilType, gruGlobalParameters, gruLocalParameters); break;
                case "TB":
                    gru = new GRUForest(timeStepHrs, id, latitude, longitude, elevation, area, slope, aspect, timeZone, write, climateStation,
                                     evapoEq, snowEngergyInputEq, canopyType, snowpackType, soilType, gruGlobalParameters, gruLocalParameters); break;
                case "TM":
                    gru = new GRUForest(timeStepHrs, id, latitude, longitude, elevation, area, slope, aspect, timeZone, write, climateStation,
                                     evapoEq, snowEngergyInputEq, canopyType, snowpackType, soilType, gruGlobalParameters, gruLocalParameters); break;
                              
                case "WL":
                    gru = new GRUWetland(timeStepHrs, id, latitude, longitude, elevation, area, slope, aspect, timeZone, write, climateStation,
                                     evapoEq, snowEngergyInputEq, canopyType, snowpackType, "WETLAND", gruGlobalParameters, gruLocalParameters); break;
                      

                default:
                    // probably should not break here...but let's throw an error (work on this...needs testing)
                    throw new Exception("GRU does not exist!");
                    
            }

            return gru;
        }

        /// <summary>
        /// A helper function which checks a line and truncates line at the 
        /// incidence of a comment character ("#")
        /// </summary>
        /// <param name="inputString">A string which may or may not contain comments</param>
        /// <returns>Truncated string, with comments removed</returns>
        private string stripComments(string inputString)
        
        {
            if (inputString.Length < 1) return inputString;

            // remove any comments (#) from line
            while (inputString.Contains("#"))
            {
                int pos = inputString.IndexOf("#");
                inputString = inputString.Substring(1, pos).Trim();
            }

            return inputString;
        }


        #endregion

    }
}
